﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SafeScript.MetaModel.Statements
{
    /// <summary>
    /// Represents a binding, i.e. "a => b".
    /// </summary>
    public class BindingOperator : Operator
    {
        public Operator Left;
        public Operator Right;

        /// <summary>
        /// May be null
        /// </summary>
        public BindingTypeDef SinkBinding;
        /// <summary>
        /// May be null
        /// </summary>
        public FunctionTypeDef SinkFunc;
        /// <summary>
        /// May be null
        /// </summary>
        public Property SinkProperty;
        /// <summary>
        /// May be null
        /// </summary>
        public BindingTypeDef SourceBinding;
        /// <summary>
        /// May be null
        /// </summary>
        public FunctionTypeDef SourceFunc;
        /// <summary>
        /// May be null
        /// </summary>
        public Property SourceProperty;
        /// <summary>
        /// May be null
        /// </summary>
        public MemberEvent SourceEvent;

        private BindingTypeDef type;
        private InstanceType instanceType;

        public override int GetPrecedence()
        {
            return 170;
        }

        public override ITypeDef ComputeType(ITypeDef goal, bool needsRef, ErrorReport report)
        {
            if (this.instanceType != null )
                return this.instanceType;
                
            //
            // Compute the type of the left side
            //

            // If the goal is a function type, then create a new function type
            // which does not restrict the return type.
            FunctionTypeDef goal3 = null;
            if (goal is InstanceType)
                goal = (goal as InstanceType).Type;
            if (goal is BindingTypeDef)
            {
                goal3 = new FunctionTypeDef();
                goal3.ParameterTypes = (goal as BindingTypeDef).ParameterTypes;
            }

            ITypeDef ltype = this.Left.ComputeType(goal3, false, report);
            if (ltype == null)
                return null;
            // A set of functions? We cannot determine which to choose -> error
            if ( ltype is FunctionSetTypeDef )
            {
                report.Add( new ErrorEntry(this.Left.Node) { Type = ErrorType.FunctionCallIsAmbiguous });
                return null;
            }
            // A member function? -> turn it into a function object
            if ( ltype is FunctionTypeDef )
                ltype = this.ToInstance(ref this.Left, ltype, report);
            // We require an instance (except for the case where it is an event)
            if (!(ltype is MemberEvent))
            {
                if (!(ltype is InstanceType))
                {
                    this.ReportNotAnObject(this.Left.Node, report);
                    return null;
                }
                ltype = (ltype as InstanceType).Type;
            }

            // Determine the type that flows from the left side of the binding
            ITypeDef source = null;
            // If it is a function -> look at the return type
            if ( ltype is FunctionTypeDef )
            {
                this.SourceFunc = ltype as FunctionTypeDef;
                // The left side function must accept at least one parameter (if it is not a binding)
                if ( this.SourceFunc.ParameterTypes.Count == 0 || this.SourceFunc.ReturnType is VoidType )
                {
                    report.Add( new ErrorEntry(this.Left.Node) { Type = ErrorType.IncompatibleTypes });
                    return null;
                }
                source = (this.SourceFunc.ReturnType as InstanceType).Type;
            }
            else if (ltype is BindingTypeDef)
            {
                this.SourceBinding = ltype as BindingTypeDef;
                // The left side function must not return void
                if ( this.SourceBinding.ReturnType is VoidType)
                {
                    report.Add(new ErrorEntry(this.Left.Node) { Type = ErrorType.IncompatibleTypes });
                    return null;
                }
                source = (this.SourceBinding.ReturnType as InstanceType).Type;
            }
            else if (ltype is MemberEvent)
            {
                this.SourceEvent = ltype as MemberEvent;
                source = this.SourceEvent.EventType;
            }
            else if (this.Left is PropertyOperator)
            {
                PropertyOperator prop = this.Left as PropertyOperator;
                if (!(prop.Object is Property))
                {
                    report.Add(new ErrorEntry(this.Left.Node) { Type = ErrorType.BindingNeedsPropertyOrFunction });
                    return null;
                }
                this.SourceProperty = prop.Object as Property;
                source = ltype;
            }
            else
            {
                report.Add(new ErrorEntry(this.Left.Node) { Type = ErrorType.BindingNeedsPropertyOrFunction });
                return null;
            }

            // Determine the type that is accepted at the right side of the binding.
            ITypeDef sink = null;
            // To do proper type inference we compute the binding type but the return type remains null since we cannot know it
            BindingTypeDef goal2 = null;
            goal2 = new BindingTypeDef();
            goal2.ParameterTypes.Add(new InstanceType() { Type = source });
            ITypeDef rtype = this.Right.ComputeType(goal2, false, report);
            if (rtype == null)
                return null;

            if ( rtype is FunctionSetTypeDef )
            {
                List<ITypeDef> ptypes = new List<ITypeDef>();
                ptypes.Add(new InstanceType() { Type = source });
                FunctionTypeDef func = (rtype as FunctionSetTypeDef).FindFunction(ptypes, report, this.Right.Node);
                if ( func == null )
                    return null;
                rtype = this.ToInstance(ref this.Right, func, report);
                report.Add( new ErrorEntry(this.Left.Node) { Type = ErrorType.FunctionCallIsAmbiguous });
                return null;
            }
            else if (rtype is InstanceType && (rtype as InstanceType).Type is FunctionTypeDef)
                SinkFunc = (rtype as InstanceType).Type as FunctionTypeDef;
            else if (rtype is InstanceType && (rtype as InstanceType).Type is BindingTypeDef)
                SinkBinding = (rtype as InstanceType).Type as BindingTypeDef;
            else if (rtype is FunctionTypeDef)
            {
                SinkFunc = rtype as FunctionTypeDef;
                rtype = this.ToInstance(ref this.Right, SinkFunc, report);
            }
            if (!(rtype is InstanceType))
            {
                this.ReportNotAnObject(this.Right.Node, report);
                return null;
            }
            rtype = (rtype as InstanceType).Type;
            if ( SinkFunc != null )
            {
                // The right side function must accept exactly one parameter
                if ( SinkFunc.ParameterTypes.Count != 1 )
                {
                    report.Add( new ErrorEntry(this.Left.Node) { Type = ErrorType.IncompatibleTypes });
                    return null;
                }
                sink = (SinkFunc.ParameterTypes[0] as InstanceType).Type;
            }
            else if (SinkBinding != null)
            {
                // The right side function must accept exactly one parameter and it must not return void (except when it is a binding)
                if (SinkBinding.ParameterTypes.Count != 1)
                {
                    report.Add(new ErrorEntry(this.Left.Node) { Type = ErrorType.IncompatibleTypes });
                    return null;
                }
                sink = (SinkBinding.ParameterTypes[0] as InstanceType).Type;
            }
            else if (this.Right is PropertyOperator)
            {
                PropertyOperator prop = this.Right as PropertyOperator;
                if (!(prop.Object is Property))
                {
                    report.Add(new ErrorEntry(this.Right.Node) { Type = ErrorType.BindingNeedsPropertyOrFunction });
                    return null;
                }
                this.SinkProperty = prop.Object as Property;
                sink = rtype;
            }
            else
            {
                report.Add(new ErrorEntry(this.Right.Node) { Type = ErrorType.IncompatibleTypes });
                return null;
            }

            // Do sink and source types match?
            if ( !source.CanBeAssignedTo(sink) )
            {
                report.Add( new ErrorEntry(this.Node) { Type = ErrorType.IncompatibleTypes });
                return null;
            }

            // Construct the return type
            this.type = new BindingTypeDef();
            if (SourceFunc != null)
                this.type.ParameterTypes = SourceFunc.ParameterTypes;
            else if (SourceBinding != null)
                this.type.ParameterTypes = SourceBinding.ParameterTypes;
            else if (this.SourceEvent != null)
                this.type.ParameterTypes.Add(this.SourceEvent.EventType);
            else
                this.type.ParameterTypes.Add(this.SourceProperty.Type);
            if (SinkFunc != null)
                this.type.ReturnType = SinkFunc.ReturnType;
            else if (SinkBinding != null)
                this.type.ReturnType = SinkBinding.ReturnType;
            else
                this.type.ReturnType = this.SinkProperty.Type;
            this.instanceType = new InstanceType() { Type = this.type };
            return this.instanceType;
        }
    }
}
